package org.xteam.sled.semantic;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.xteam.parser.runtime.AstList;
import org.xteam.parser.runtime.IErrorReporter;
import org.xteam.parser.runtime.Span;
import org.xteam.sled.ast.OpName;
import org.xteam.sled.ast.StringOpName;
import org.xteam.sled.model.IDisjunct;
import org.xteam.sled.model.LabelledDisjunct;
import org.xteam.sled.model.LabelledPattern;
import org.xteam.sled.model.LabelledSequence;
import org.xteam.sled.semantic.exp.ExpConst;


public class SemanticContext implements IEvaluationContext {
	
	// initialy contains "epsilon" -> Pattern.EPSILON
	private Map<String, IDenotable> symtab = new HashMap<String, IDenotable>(); 
	private List<TokenClass> tokenClasses = new ArrayList<TokenClass>();
	private Map<String, InstructionConstructor> constructors = new HashMap<String, InstructionConstructor>();
	private ConstructorType instructionConstructorType;
	private BitNumbering bitNumbering = new BitNumbering();
	private FieldInformations fieldInfo = new FieldInformations();
	private Set<String> relocatable = new HashSet<String>();
	private List<ConstructorType> constructorTypes = new ArrayList<ConstructorType>();
	
	// From GlobalState
	public int pcUnitBits = 32;
	
	private IErrorReporter reporter;
	
	
	public SemanticContext(IErrorReporter reporter) {
		this.reporter = reporter;
		bindUnbound("epsilon", new LabelledPattern("epsilon", new LabelledDisjunct(new LabelledSequence())), null);
	}
	
	public void reportError(Span span, String msg) {
		reporter.reportError(span.from(), span.length(), msg);
	}
	
	public void reportWarning(Span span, String msg) {
		reporter.reportWarning(span.from(), span.length(), msg);
	}

	public BitNumbering bitNumbering() {
		return bitNumbering;
	}
	
	public void bindUnbound(String name, IDenotable f, Span span) {
		if (symtab.containsKey(name)) {
			reportError(span, "identifier " + name + " redefined");
			bindError(name);
		} else {
			bind(name, f);
		}
	}

	public void bindUnboundError(String name, Span span) {
		symtab.put(name, new DenotableError());
		if (symtab.containsKey(name)) {
			reportError(span, "identifier " + name + " redefined");
		}
		bindError(name);
	}
	
	public void bind(String name, IDenotable f) {
		symtab.put(name, f);
	}
	
	public void bindError(String name) {
		symtab.put(name, new DenotableError());
	}
	

	public Field optField(String name) {
		IDenotable denotable = lookupOpt(name);
		if (denotable instanceof Field)
			return (Field) denotable;
		return null;
	}
	
	public IDenotable lookup(String name, Span span) {
		if (! symtab.containsKey(name)) {
			reportError(span, "identifier " + name + " is undefined");
			return null;
		}
		return symtab.get(name);
	}
	
	@Override
	public IDenotable lookupGlobal(String name, Span span) {
		return lookup(name, span);
	}
	
	public IDenotable lookupOpt(String name) {
		return symtab.get(name);
	}

	public void addRelocatable(String reloc) {
		relocatable.add(reloc);
	}

	public void addTokenClass(TokenClass cls) {
		tokenClasses.add(cls);
	}

	public FieldInformations getFieldInformations() {
		return fieldInfo;
	}

	public void setChecking(Field field, int value) {
		fieldInfo.setChecking(field, value);
	}

	public void bindSpec(Field field, Map<String, Integer> spec, boolean fullMap, Span span) {
		if (fieldInfo.hasNames(field)) {
			reportError(span,
					"Field names for field " + field.name() + " already specified");
		} else {
			fieldInfo.setNames(field, spec, fullMap);
		}
	}
	
	public List<OpcodeElement> explodeOpcode(AstList<OpName> opnames) {
		List<OpcodeElement> elements = new ArrayList<OpcodeElement>();
		elements.add(new OpcodeElement("", new HashMap<String, IDenotable>()));
		for (OpName opName : opnames) {
			if (opName instanceof StringOpName) {
				elements = addInterpretation(opName.getValue(), opName.getValue(), null, elements, opName.span());
			} else {
				IDenotable denotable = lookupOpt (opName.getValue());
				if (denotable == null) {
					reportWarning(opName.span(),
							opName.getValue() + " is undefined, assuming \"" + opName.getValue() + "\"");
					elements = addInterpretation(opName.getValue(), opName.getValue(), null, elements, opName.span());
				} else {
					elements = multiplyDenotable(opName.getValue(), denotable, elements, opName.span());
				}
			}
		}
		return elements;
	}

	private List<OpcodeElement> addInterpretation(String originalName, String expandedName, IDenotable meaning,
			List<OpcodeElement> elements, Span span) {
		List<OpcodeElement> result = new ArrayList<OpcodeElement>();
		for (OpcodeElement element : elements) {
			result.add(new OpcodeElement(element.name() + expandedName, addBinding(element.env(), originalName, meaning, span)));
		}
		return result;
	}
	
	private Map<String, IDenotable> addBinding(Map<String, IDenotable> oldEnv, String originalName, IDenotable meaning, Span span) {
		if (meaning != null) {
			if (oldEnv.containsKey(originalName)) {
				reportWarning(span,
						"Replicated opcode element " + originalName);
			}
			Map<String, IDenotable> newEnv = new HashMap<String, IDenotable>(oldEnv);
			newEnv.put(originalName, meaning);
			return newEnv;
		}
		return oldEnv;
	}

	private List<OpcodeElement> multiplyDenotable(String value, IDenotable denotable, List<OpcodeElement> elements, Span span) {
		if (denotable instanceof Field) {
			Field field = (Field) denotable;
			if (fieldInfo.hasNames(field)) {
				Map<String, Integer> spec = fieldInfo.namesOf(field);
				List<OpcodeElement> newInterps = new ArrayList<OpcodeElement>();
				for (String name : spec.keySet()) {
					IDenotable meaning = new Temporary (new ExpConst(spec.get(name)), field);
					newInterps.addAll(addInterpretation(field.name(), name, meaning, elements, span));
				}
				elements = newInterps;
			} else {
				reportError(span, "Used field `" + field.name()
						+ "' in opcode without supplying field names");
			}
		} else if (denotable instanceof LabelledPattern) {
			LabelledPattern p = (LabelledPattern) denotable;
			List<OpcodeElement> newInterps = new ArrayList<OpcodeElement>();
			for (IDisjunct dis : p.getDisjuncts()) {
				String newname = dis.name() != null ? dis.name() :  p.name();
				newInterps.addAll(addInterpretation(p.name(), newname, new LabelledPattern(newname, (LabelledDisjunct)dis), elements, span));
			}
			elements = newInterps;
		} else {
			reportError(span, "expecting field or pattern in opcode");
		}
		return elements;
	}

	public Operand makeOperand(String name, boolean isSigned, Span span) {
		String originalName = name;
		IDenotable denotable = null;
		while (name.length() > 0 && (denotable = lookupOpt(name)) == null) {
			char lastChar = name.charAt(name.length()-1);
			if (lastChar == '_' || Character.isDigit(lastChar))
				name = name.substring(0, name.length()-2);
			else
				break;
		}
		if (denotable == null) {
			return new Operand(originalName,
					new IntegerOperand(isSigned, /* width */ -1),
					null, relocatable.contains(originalName));
		}
		if (denotable instanceof Field) {
			Field field = (Field) denotable;
			return new Operand(originalName,
					new IntegerOperand(isSigned, field.size()),
					field, relocatable.contains(originalName));
		}
		if (denotable instanceof ConstructorType) {
			return new Operand(originalName,
					new InstanceOperand((ConstructorType)denotable),
                    null, false);
		}
		reportError(span, "unbound name, field, or constructor input " + name);
		return null;
	}

	public void addConstructor(InstructionConstructor instructionConstructor) {
		constructors.put(instructionConstructor.name(), instructionConstructor);
	}

	public ConstructorType getInstructionConstructorType() {
		if (instructionConstructorType == null) {
			instructionConstructorType = new ConstructorType("<instruction>");
			constructorTypes.add(instructionConstructorType);
		}
		return instructionConstructorType;
	}

	@Override
	public InstructionConstructor lookupConstructor(String name, Span span) {
		InstructionConstructor iconst = constructors.get(name);
		if (iconst == null)
			reportError(span, "cannot find constructor \'" + name + "\'");
		return iconst;
	}

	@Override
	public Map<String, Integer> getNamesOf(Field field) {
		return fieldInfo.namesOf(field);
	}

	public List<ConstructorType> getConstructorTypes() {
		return constructorTypes;
	}

	public void addConstructorType(ConstructorType constype) {
		constructorTypes.add(constype);
	}

}
